"""
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    created by lane.chang on '07/09/2022'
    comment: 自定义form
"""

# custom_form.py
import sys, os
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *


# ----------------- 底层字段描述 -----------------
class Field:
    """字段声明"""
    def __init__(self, key, label, typ=str, default=None, choices=None,
                 required=False, placeholder='', stretch=1, readonly=False):
        self.key, self.label, self.typ = key, label, typ
        self.default, self.choices = default, choices
        self.required = required
        self.placeholder = placeholder
        self.stretch = stretch
        self.readonly = readonly


# ----------------- 控件工厂 -----------------
class ControlFactory:
    @staticmethod
    def create(field: Field):
        """根据字段类型返回 (控件, 取值函数, 赋值函数)"""
        if field.typ == bool:
            w = QCheckBox()
            get_val, set_val = w.isChecked, w.setChecked
            if field.default is not None:
                set_val(field.default)
            return w, get_val, set_val

        if field.choices:
            w = QComboBox()
            w.addItems(field.choices)
            if field.default is not None:
                w.setCurrentText(str(field.default))
            get_val, set_val = w.currentText, w.setCurrentText
            return w, get_val, set_val

        if field.typ == int:
            w = QSpinBox()
            w.setRange(-999999, 999999)
            if field.default is not None:
                w.setValue(field.default)
            get_val, set_val = w.value, w.setValue
            return w, get_val, set_val

        if field.typ == float:
            w = QDoubleSpinBox()
            w.setDecimals(2)
            w.setRange(-999999.99, 999999.99)
            if field.default is not None:
                w.setValue(field.default)
            get_val, set_val = w.value, w.setValue
            return w, get_val, set_val

        if field.typ == QFileInfo:  # 文件路径
            container = QWidget()
            h = QHBoxLayout(container); h.setContentsMargins(0,0,0,0)
            line = QLineEdit(); line.setPlaceholderText(field.placeholder)
            btn = QPushButton('…')
            h.addWidget(line, 1)
            h.addWidget(btn)

            def pick():
                f, _ = QFileDialog.getOpenFileName(container, '选择文件')
                if f:
                    line.setText(f)
            btn.clicked.connect(pick)
            if field.default:
                line.setText(field.default)
            return container, lambda: QFileInfo(line.text()), lambda v: line.setText(v.filePath())

        if field.typ == QDate:
            w = QDateEdit()
            w.setCalendarPopup(True)
            w.setDisplayFormat('yyyy-MM-dd')
            w.setDate(field.default or QDate.currentDate())
            get_val, set_val = w.date, w.setDate
            return w, get_val, set_val

        # 默认当 str
        w = QLineEdit()
        w.setPlaceholderText(field.placeholder)
        if field.default is not None:
            w.setText(str(field.default))
        w.setReadOnly(field.readonly)
        get_val, set_val = w.text, w.setText
        return w, get_val, set_val


# ----------------- 真正表单 -----------------
class Form(QWidget):
    def __init__(self, fields: list, spacing=8, label_width=100, parent=None):
        super().__init__(parent)
        self._fields = fields
        self._ctl_map = {}      # key -> (widget, getter, setter)
        self._required = set(f.key for f in fields if f.required)

        # 主布局
        self.form_layout = QFormLayout(self)
        self.form_layout.setVerticalSpacing(spacing)
        self.form_layout.setLabelAlignment(Qt.AlignRight)

        # 统一标签列宽
        self.label_width = label_width
        self._build_rows()

    def _build_rows(self):
        for f in self._fields:
            w, get_val, set_val = ControlFactory.create(f)
            self._ctl_map[f.key] = (w, get_val, set_val)
            label = QLabel(f.label)
            label.setFixedWidth(self.label_width)
            if f.required:
                label.setText(f'{f.label} <span style="color:red;">*</span>')
            self.form_layout.addRow(label, w)

    # 公共 API
    def get_values(self) -> dict:
        """返回 {'key': 值}"""
        return {k: get() for k, (_, get, _) in self._ctl_map.items()}

    def set_values(self, data: dict):
        """批量赋值"""
        for k, v in data.items():
            if k in self._ctl_map:
                self._ctl_map[k][2](v)

    def clear(self):
        """恢复默认值"""
        for f in self._fields:
            _, _, setter = self._ctl_map[f.key]
            if f.typ == bool:
                setter(False)
            elif f.typ in (int, float):
                setter(f.default or 0)
            elif f.typ == QDate:
                setter(f.default or QDate.currentDate())
            else:
                setter(str(f.default or ''))

    def validate(self) -> bool:
        """简单必填校验"""
        for k in self._required:
            val = self._ctl_map[k][1]()
            if (isinstance(val, str) and val.strip() == '') or val is None:
                QMessageBox.warning(self, '提示', f'【{self._fields[k].label}】不能为空！')
                return False
        return True


# ----------------- 可折叠组 -----------------
class FormGroup(QGroupBox):
    def __init__(self, title, form: Form, parent=None):
        super().__init__(title, parent)
        self.setCheckable(True)
        self.setChecked(True)
        self.form = form
        v = QVBoxLayout(self)
        v.addWidget(form)
        self.toggled.connect(lambda on: self.form.setVisible(on))


# ----------------- Demo 窗口 -----------------
class DemoWindow(QWidget):
    def __init__(self):
        super().__init__()
        self.setWindowTitle('自定义 Form 框架 Demo')

        # 1. 定义字段
        fields = [
            Field('name', '姓名', str, required=True, placeholder='请输入姓名'),
            Field('age', '年龄', float, default=18),
            Field('gender', '性别', str, choices=['M', 'F'], default='M'),
            Field('birth', '生日', QDate),
            Field('avatar', '头像', QFileInfo, placeholder='点击右侧按钮选择图片'),
            Field('vip', '启用VIP', bool, default=False),
        ]

        # 2. 创建表单
        self.form = Form(fields, spacing=10, label_width=40)

        # 3. 按钮
        btn_bar = QHBoxLayout()
        reset_btn = QPushButton('重置')
        save_btn = QPushButton('保存')
        reset_btn.clicked.connect(self.form.clear)
        save_btn.clicked.connect(self.save)
        btn_bar.addWidget(reset_btn)
        btn_bar.addWidget(save_btn)
        btn_bar.addStretch()

        # 4. 主布局
        v = QVBoxLayout(self)
        v.addWidget(QLabel('<h3>用户信息</h3>'))
        v.addWidget(self.form)
        v.addLayout(btn_bar)

    def save(self):
        if not self.form.validate():
            return
        data = self.form.get_values()
        print('保存数据:', data)
        QMessageBox.information(self, '成功', f'已获取数据:\n{data}')


# ----------------- main -----------------
if __name__ == '__main__':
    app = QApplication(sys.argv)
    w = DemoWindow()
    w.resize(420, 300)
    w.show()
    sys.exit(app.exec_())